On this page

Skip to content

Simplifying Parameter Validation with CallerArgumentExpression

When developing packages, to ensure that constructor or method parameters meet expectations, we typically perform parameter validation for null or empty strings. To simplify operations and standardize error messages, I usually write a static ExceptionUtils class to perform these checks, as shown in the example below:

csharp
public static class ExceptionUtils {
    public static void ThrowIfNull<T>(Expression<Func<T?>> expression) {
        _ = expression.Compile().Invoke()
            ?? throw new ArgumentNullException(GetMemberName(expression));
    }

    public static void ThrowIfNullOrWhiteSpace(Expression<Func<string?>> expression) {
        string? value = expression.Compile().Invoke();
        if (string.IsNullOrWhiteSpace(value)) {
            throw new ArgumentException("Must not be null or whitespace.", GetMemberName(expression));
        }
    }

    private static string GetMemberName<T>(Expression<Func<T>> expression) {
        if (expression.Body is not MemberExpression expressionBody) {
            throw new ArgumentException("Expression error.", nameof(expression));
        }
        return expressionBody.Member.Name;
    }
}

This allows for parameter checking in the following way. Using Expression is intended to avoid passing both the parameter value and the parameter name, thereby simplifying usage:

csharp
public void Method(string str) {
    ExceptionUtils.ThrowIfNullOrWhiteSpace(() => str);
}

However, .NET 6 introduced the Nullable reference type checking mechanism. Typically, after we perform a null check, the compiler can recognize that the variable will not be null:

csharp
string ToLower(string? str) {
    if (str is null) {
        throw new ArgumentNullException(nameof(str));
    }

    // Since null has been checked, the compiler will no longer issue a null warning for str
    return str.ToLower();
}

But because my ExceptionUtils uses Expression rather than checking the parameter directly, I cannot add [NotNull] to the parameter to let the compiler know that the checked parameter is not null. Therefore, I adjusted the code as follows:

csharp
public static class ExceptionUtils {
    public static void ThrowIfNull<T>(Expression<Func<T>> expression, [DoesNotReturnIf(true)] bool isNull = true) {
        _ = expression.Compile().Invoke()
            ?? throw new ArgumentNullException(GetMemberName(expression));
    }

    public static void ThrowIfNullOrWhiteSpace(Expression<Func<string?>> expression, [DoesNotReturnIf(true)] bool isNull = true) {
        string? value = expression.Compile().Invoke();
        if (string.IsNullOrWhiteSpace(value)) {
            throw new ArgumentException("Must not be null or whitespace.", GetMemberName(expression));
        }
    }

    private static string GetMemberName<T>(Expression<Func<T>> expression) {
        return expression.Body is not MemberExpression expressionBody
            ? throw new ArgumentException("Expression error.", nameof(expression))
            : expressionBody.Member.Name;
    }
}

The isNull parameter is included only because using [DoesNotReturn] directly triggers the warning: "Methods marked with [DoesNotReturn] should not return." I had to use [DoesNotReturnIf(true)] combined with the meaningless isNull parameter to handle this. Of course, I have never been very satisfied with the above solution. After .NET 6 and .NET 7, the official team provided some simplified static check methods:

csharp
// Added in .NET 6
ArgumentNullException.ThrowIfNull(object? argument, string? paramName = null);

// Added in .NET 7
ArgumentNullException.ThrowIfNullOrEmpty(string? argument, string? paramName = null);
ArgumentNullException.ThrowIfNullOrWhiteSpace(string? argument, string? paramName = null);

// Added in .NET 7
ArgumentException.ThrowIfNullOrEmpty(string? argument, string? paramName = null);
ArgumentException.ThrowIfNullOrWhiteSpace(string? argument, string? paramName = null);

I recently looked at the source code for ThrowIfNull, and the [CallerArgumentExpression] within it reminded me of a book I borrowed from a junior colleague, which mentioned that it seems to be used to automatically retrieve parameter names.

csharp
public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) {
    if (argument is null) {
        Throw(paramName);
    }
}

Therefore, I wrote the following program to test it:

csharp
string? str = "";
Console.Write("When paramName is not passed, ");
TestCallerArgumentExpression(str);

Console.Write("When paramName is passed, ");
TestCallerArgumentExpression(str, "str2");

void TestCallerArgumentExpression(object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) {
    Console.WriteLine("paramName:" + paramName);
}

The results are as follows:

text
When paramName is not passed, paramName: str
When paramName is passed, paramName: str2

When paramName is not passed, it automatically uses the variable name of the argument passed to argument as the value for paramName. This approach is more concise than my original Expression solution and also allows the use of [NotNull] to support Nullable reference checks.

Change Log

  • 2024-10-13 Initial document creation.